package com.gitee.wsl.time.schedule.cron

import com.gitee.wsl.time.copy
import com.gitee.wsl.time.daysIn
import com.gitee.wsl.time.millisecond
import com.gitee.wsl.time.months
import com.gitee.wsl.time.plus
import com.gitee.wsl.time.years
import kotlinx.datetime.LocalDateTime
import kotlin.math.min
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
import kotlin.time.ExperimentalTime


fun interface NearDateTimeCalculator {
    /**
     * @return pair of near [LocalDateTime] for this checker and [Boolean] flag that all previous calculations must be
     * recalculated
     */
    fun calculateNearTime(
        relativelyTo: LocalDateTime
    ): Pair<LocalDateTime, Boolean>?
}

internal class CommonNearDateTimeCalculator<T>(
    private val times: Array<T>,
    private val partGetter: (LocalDateTime) -> T,
    private val partSetter: (LocalDateTime, T) -> LocalDateTime?
) : NearDateTimeCalculator where T : Comparable<T>, T : Number {
    /**
     * @return pair of near [LocalDateTime] for this checker and [Boolean] flag that all previous calculations must be
     * recalculated
     */
    override fun calculateNearTime(
        relativelyTo: LocalDateTime
    ): Pair<LocalDateTime, Boolean>? {
        val currentData = partGetter(relativelyTo)
        val greaterOrEquals = times.firstOrNull { it >= currentData }
        val newDateTime = when (greaterOrEquals) {
            null -> partSetter(relativelyTo, times.first()) ?: return null
            currentData -> relativelyTo
            else -> partSetter(relativelyTo, greaterOrEquals) ?: return null
        }
        return if (newDateTime == relativelyTo) {
            relativelyTo to false
        } else {
            newDateTime to true
        }
    }
}

internal fun NearDateTimeCalculatorMillis(
    times: Array<Short>
) = CommonNearDateTimeCalculator(
    times,
    { it.millisecond.toShort() },
    { localDateTime, newOne ->
        (if (newOne < localDateTime.millisecond) {
            localDateTime + 1.seconds
        } else {
            localDateTime
        }).copy(nanosecond = newOne.toInt())
    }
)

internal fun NearDateTimeCalculatorSeconds(
    times: Array<Byte>
) = CommonNearDateTimeCalculator(
    times,
    { it.second.toByte() },
    { localDateTime, newOne ->
        (if (newOne < localDateTime.second) {
            localDateTime.plus(1.minutes)
        } else {
            localDateTime
        }).copy(second = newOne.toInt(), nanosecond = 0)
    }
)

internal fun NearDateTimeCalculatorMinutes(
    times: Array<Byte>
) = CommonNearDateTimeCalculator(
    times,
    { it.minute.toByte() },
    { localDateTime, newOne ->
        (if (newOne < localDateTime.minute) {
            localDateTime.plus(1.hours)
        } else {
            localDateTime
        }).copy(minute = newOne.toInt(), second = 0, nanosecond = 0)
    }
)

internal fun NearDateTimeCalculatorHours(
    times: Array<Byte>
) = CommonNearDateTimeCalculator(
    times,
    { it.hour.toByte() },
    { localDateTime, newOne ->
        (if (newOne < localDateTime.hour) {
            localDateTime.plus(1.days)
        } else {
            localDateTime
        }).copy(hour = newOne.toInt(), minute = 0, second = 0, nanosecond = 0)
    }
)

@OptIn(ExperimentalTime::class)
internal fun NearDateTimeCalculatorDays(
    times: Array<Byte>
) = CommonNearDateTimeCalculator(
    times,
    { (it.dayOfMonth - 1).toByte() }, // index1, so, decrease
    { localDateTime, newOne ->
        val localDateTime = (if (newOne < localDateTime.dayOfMonth) {
            localDateTime.plus(1.months)
        } else {
            localDateTime
        })
        val newDateTime = localDateTime.copy(
            dayOfMonth = min(localDateTime.month.daysIn(localDateTime.year), newOne.toInt() + 1), // index1
            hour = 0,
            minute = 0,
            second = 0,
            nanosecond = 0
        )
        // If day of month has not been changed, use old LocalDateTime due to no changes required
        if (newDateTime.dayOfMonth == localDateTime.dayOfMonth) {
            localDateTime
        } else {
            newDateTime
        }
    }
)

internal fun NearDateTimeCalculatorMonths(
    times: Array<Byte>
) = CommonNearDateTimeCalculator(
    times,
    { it.month.ordinal.toByte() },
    { localDateTime, newOne ->
        (if (newOne < localDateTime.month.ordinal) {
            localDateTime.plus(1.years)
        } else {
            localDateTime
        }).copy(
            month = newOne.toInt() + 1, // index1
            dayOfMonth = 1, // index1
            hour = 0,
            minute = 0,
            second = 0,
            nanosecond = 0
        )
    }
)

internal fun NearDateTimeCalculatorWeekDays(
    times: Array<Byte>
) = CommonNearDateTimeCalculator(
    times,
    { it.dayOfWeek.ordinal.toByte() },
    { localDateTime, newOne ->
        val currentDayOfWeek = localDateTime.dayOfWeek.ordinal
        if (newOne.toInt() == currentDayOfWeek) return@CommonNearDateTimeCalculator localDateTime
        (if (newOne < currentDayOfWeek) {
            localDateTime.plus(7.days - (currentDayOfWeek - newOne).days)
        } else {
            localDateTime.plus(newOne.toInt().days - currentDayOfWeek.days)
        }).copy(
            hour = 0,
            minute = 0,
            second = 0,
            nanosecond = 0
        )
    }
)

internal fun NearDateTimeCalculatorYears(
    times: Array<Int>
) = CommonNearDateTimeCalculator(
    times,
    { it.year },
    { localDateTime, newOne ->
        val currentYear = localDateTime.year
        if (newOne == currentYear) return@CommonNearDateTimeCalculator localDateTime
        (if (newOne < currentYear) {
            null
        } else {
            localDateTime.plus((newOne - currentYear).years)
        }) ?.copy(
            month = 1, // index1
            dayOfMonth = 1, // index1
            hour = 0,
            minute = 0,
            second = 0,
            nanosecond = 0
        )
    }
)